package org.hawk.gwt.ppc.reflect;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.hawk.gwt.ppc.Log;
import org.hawk.gwt.ppc.ProcessingUnitsManager;
import org.hawk.gwt.ppc.javacc.JavaParserTreeConstants;
import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.reflect.lang.ClassDelegate;
import org.hawk.gwt.ppc.scanner.Processor;
import org.hawk.gwt.ppc.scanner.Token;
import org.hawk.gwt.ppc.scanner.TokenChain;
import org.hawk.gwt.ppc.scanner.utils.JavaClassDescriptorUtils;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.StringUtils;
/**
 * Replaces all reflection calls and classes with
 * theirs analogs from the internal implementation.
 * All {@link Object#getClass()} method calls and 
 * [ClassName].class properties accesses will be wrapped
 * with internal {@link ClassDelegate#forClass(Class)}
 * calls. Java class will be used as parameter.
 * @author alex.bereznevatiy@gmail.com
 */
public class ReflectionCallsProcessor implements Processor {
	/*
	 * used for debug only
	 */
	private static Set<String> modifiedClassNames;
	
	private static final Set<String> reflectionMethodNames;
	
	static {
		Set<String> rez = new HashSet<String>();
		rez.add("forName");
		rez.add("newInstance");
		rez.add("isInstance");
		rez.add("isAssignableFrom");
		rez.add("getClassLoader");
		rez.add("getPackage");
		rez.add("getInterfaces");
		rez.add("getModifiers");
		rez.add("getSimpleName");
		rez.add("getCanonicalName");
		rez.add("isAnonymousClass");
		rez.add("getFields");
		rez.add("getMethods");
		rez.add("getConstructors");
		rez.add("getField");
		rez.add("getMethod");
		rez.add("getConstructor");
		rez.add("getDeclaredFields");
		rez.add("getDeclaredMethods");
		rez.add("getDeclaredConstructors");
		rez.add("getDeclaredField");
		rez.add("getDeclaredMethod");
		rez.add("getDeclaredConstructor"); 

		reflectionMethodNames = Collections.unmodifiableSet(rez);
	}
	
	private int classExpression = -1;

	public void visitStart(TokenChain<? extends Token> iterator,
			int kind) {}

	private boolean isReflectionMethodCall(TokenChain<? extends Token> iterator) {
		int index = iterator.index();
		boolean rez = checkForReflectionCall(iterator);
		iterator.moveTo(index);
		return rez;
	}

	private boolean checkForReflectionCall(TokenChain<? extends Token> iterator) {
		skipParamTypesForward(iterator);
		if(!iterator.hasNext()||
			!iterator.current().getToken().equals(".")){
			return false;
		}
		return reflectionMethodNames.contains(
				iterator.next().getToken());
	}

	private void skipParamTypesForward(TokenChain<? extends Token> iterator) {
		if(!iterator.hasNext()||!iterator.next().getToken().equals("<"))return;
		int deep = 1;
		while(deep>0){
			if(iterator.next().getToken().equals(">")){
				deep--;
			}
			else if(iterator.current().getToken().equals("<")){
				deep++;
			}
		}
		iterator.next();
	}

	private void processClass(TokenChain<? extends Token> iterator) {
		Token tok = iterator.current();
		if(iterator.previous().getToken().equals(".")&&
			!processJavaLangPackage(iterator)){
			return;
		}
		replace(tok,Class.class.getSimpleName(),
				ClassDelegate.class.getName());
		classExpression = tok.getIndex();
	}

	private void replace(Token tok,String val,String replacement) {
		tok.setToken(StringUtils.replaceAll(
			tok.getRawToken(), val, replacement));
		logClassIsModified();
	}

	private boolean processJavaLangPackage(TokenChain<? extends Token> iterator) {
		if(!iterator.previous().getToken().equals("lang")||
			!iterator.previous().getToken().equals(".")||
			!iterator.previous().getToken().equals("java")||
			iterator.previous().getToken().equals(".")){
			return false;
		}
		
		replace(iterator.next(),"java","");
		replace(iterator.next(),".","");
		replace(iterator.next(),"lang","");
		replace(iterator.next(),".","");
		return true;
	}

	public void visitEnd(TokenChain<? extends Token> iterator,int kind) throws ParseException, IOException {
		if(kind==JavaParserTreeConstants.JJTPRIMARYSUFFIX&&
			iterator.previous().getToken().equals("(")){
			iterator.previous();
			iterator.previous();
			if(iterator.previous().getToken().equals(Class.class.getSimpleName())&&
				isReflectionMethodCall(iterator)){
				processClass(iterator);
				return;
			}
			iterator.next();
			iterator.next();
		}
		if(kind==JavaParserTreeConstants.JJTPRIMARYSUFFIX){
			processReflectionExpression(iterator);
		}
		else if(kind==JavaParserTreeConstants.JJTPRIMARYEXPRESSION&&
			classExpression>=0){
			processClassExpression(iterator);
		}
	}
	
	private void processClassExpression(TokenChain<? extends Token> iterator){
		while(iterator.previous().getIndex()>classExpression&&
			!isReflectionMethodCall(iterator));
		while(iterator.previous().getIndex()>=classExpression){
			if(isReflectionMethodCall(iterator)){
				classExpression = -1;
				return;
			}
		}
		iterator.moveTo(classExpression+2);
		if(!iterator.next().getToken().equals("(")){
			return;
		}
		int deep = 1;
		while(deep>0){
			if(iterator.next().getToken().equals("(")){
				deep++;
			}
			else if(iterator.next().getToken().equals(")")){
				deep--;
			}
		}
		if(!iterator.next().equals(".")){
			replace(iterator.current(),")",").toClass()");
		}
		classExpression = -1;
	}

	private void processReflectionExpression(
			TokenChain<? extends Token> iterator){
		while(!iterator.previous().getToken().equals(".")){
			if(!iterator.hasPrevious()){
				return;
			}
		}
		iterator.previous();
		if(isReflectionMethodCall(iterator)){
			processReflectionCallExpression(iterator);
		}
	}

	private void processReflectionCallExpression(
			TokenChain<? extends Token> iterator) {
		try {
			iterator.next();
			int end = iterator.index();
			int start = getReflectionExpressionStart(iterator);
			if(iterator.next().isModified()){
				return;
			}
			String type = JavaClassDescriptorUtils.evaluateReturnTypeOfExpression(
					iterator, guessClassName(), start, end);
			if(Name.getRawName(type).equals(Class.class.getName())){
				processReflectionExpression(iterator, start,end-1);
			}
		}
		catch(Throwable th){
			Log.internalError("Unable to evaluate reflection expression because " +
				"of underline error.\nBecause of this error some of the " +
				"reflection calls may not be correctly recognized by " +
				"GWT compiller.\n", th);
		}
	}

	private int getReflectionExpressionStart(
			TokenChain<? extends Token> iterator) {
		int deep = 1;
		while(deep>0){
			String token = iterator.previous().getToken();
			if(token.equals("return")){
				deep--;
			}
			if(token.length()!=1)continue;
			char t = token.charAt(0);
			if(t==')'||t=='}'||t==']'){
				deep++;
			}
			else if(t=='('||t=='{'||t==';'){
				deep--;
			}
			if(deep==1&&t=='='){
				deep--;
			}
		}
		return iterator.index()+1;
	}

	private void processReflectionExpression(
			TokenChain<? extends Token> iterator, int start,int end) {
		iterator.moveTo(start);
		Token startTok = iterator.current();
		iterator.moveTo(end);
		Token endTok = iterator.current();
		StringBuilder prefix = new StringBuilder();
		prefix.append(ClassDelegate.class.getName());
		prefix.append(".forClass(");
		prefix.append(startTok.getToken());
		replace(startTok, startTok.getToken(), prefix.toString());
		replace(endTok,endTok.getRawToken(),endTok.getRawToken()+')');
	}

	private String guessClassName() {
		String className = ProcessingUnitsManager.getInstance().getCurrentResource();
		if(className==null){
			// this branch will not work on real runtime
			// but useful for unit testcases.
			className = System.getProperty("hawk.gwt.ppc.current_class");
		}
		return className;
	}
	
	private boolean isDebugForbided = false;
	private void logClassIsModified(){
		if(isDebugForbided)return;
		if(!Log.isDebugEnabled()){
			isDebugForbided = true;
			return;
		}
		if(modifiedClassNames==null){
			modifiedClassNames = new HashSet<String>();
		}
		String className = guessClassName();
		if(modifiedClassNames.contains(className)){
			return;
		}
		modifiedClassNames.add(className);
		Log.debug("\t"+getClass().getSimpleName()+" processing "+className);
	}
}
